import time
import asyncio
import aiohttp
import aiohttp_socks
import ccxt.pro as ccxt_pro
from retrying import retry


class Exchange:
    support_ids = ["binance", ]

    def __init__(self,
                 id,
                 logger,
                 quote_symbol="USDT",
                 proxy_url=None,
                 api_key=None,
                 api_secret=None,
                 lock=None,
                 exit_value=None):
        self.id = id
        assert self.id in Exchange.support_ids
        self.quote_symbol = quote_symbol
        # external api related
        self.proxy_url = proxy_url
        self.api_key = api_key
        self.api_secret = api_secret
        # multiprocess related
        self.lock = lock
        self.exit_value = exit_value
        # others
        self.logger = logger
        self.is_init = False

    async def set_session(self, proxy_url):
        self.proxy_url = proxy_url
        connector = None
        if proxy_url:
            connector = aiohttp_socks.ProxyConnector.from_url(proxy_url)
        self.session = aiohttp.ClientSession(connector=connector)

    def load_symbols(self, ):
        assert self.is_init
        self.spot_symbols = {
            i for i in self.api.markets if i[-len(self.quote_symbol)-1:] == f"/{self.quote_symbol}" and \
                self.api.markets[i]["active"] == True
        }
        self.logger.info(f"load {self.id}, spot symbols length is {len(self.spot_symbols)}")

    @retry(stop_max_attempt_number=3, wait_fixed=1000)
    async def load_markets(self, reload=False):
        await self.api.load_markets(reload=reload)
        if self.future_api != self.api:
            await self.future_api.load_markets(reload=reload)
        self.is_init = True
        self.load_symbols()

    async def init_exchange(self, ):
        await self.set_session(self.proxy_url)
        api_config = {
            "session": self.session,
        }
        if self.api_key is not None:
            api_config["apiKey"] = self.api_key
            api_config["secret"] = self.api_secret
            api_config["nonce"] = ccxt_pro.Exchange.milliseconds
        self.api = getattr(ccxt_pro, self.id)(api_config)
        if self.id + "futures" in ccxt_pro.exchanges:
            self.future_api = getattr(ccxt_pro, self.id + "futures")(api_config)
        else:
            self.future_api = self.api
        await self.load_markets()

    async def exit_exchange(self, ):
        if self.future_api != self.api:
            await self.future_api.close()
        await self.api.close()
        await self.session.close()

    def _symbols(self, symbols=None):
        assert self.is_init
        if symbols is None:
            symbols = self.spot_symbols
        if isinstance(symbols, str):
            symbols = [symbols]
        return symbols

    # 返回指定symbols的tickers, 如果不传入symbols则返回所有quote_symbol可交易的symbol
    def get_tickers(self, symbols=None):
        select_symbols = self._symbols(symbols)
        return dict((i, self.api.tickers[i]) for i in select_symbols if i in self.api.tickers)

    # 返回指定markets的tickers, 如果不传入symbols则返回所有quote_symbol可交易的symbol
    def get_markets(self, symbols=None):
        select_symbols = self._symbols(symbols)
        return dict((i, self.api.markets[i]) for i in select_symbols if i in self.api.markets)
    
    async def fetch_balance(self, ):
        try:
            return await self.api.fetchBalance()
        except Exception as e:
            self.logger.info(f"fetch balance failed {e}")
            return None
    
    # price=-1时代表市价单, 否则为限制价单, 这个api中amount都是以base_coin作为单位的
    async def create_order(self, symbol, amount, price, side):
        try:
            if price == -1:
                return await self.api.createOrder(symbol, "market", side, amount, price)
            else:
                return await self.api.createOrder(symbol, "limit", side, amount, price)
        except Exception as e:
            self.logger.info(f"{side} {symbol} amount {amount} price {price} failed {e}")
            return None

    # 使用cost市价买入
    async def create_buy_market_order_with_cost(self, symbol, cost):
        try:
            return await self.api.createMarketOrderWithCost(symbol, "buy", cost)
        except Exception as e:
            self.logger.info(f"buy {symbol} cost {cost} failed {e}")
            return None
        
    async def fetch_order(self, symbol, order_id):
        try:
            return await self.api.fetchOrder(order_id, symbol)
        except Exception as e:
            self.logger.info(f"fetch order {order_id} {symbol} failed {e}")
            return None

    # orders are ccxt order structures.
    async def fetch_orders_by_orders(self, orders):
        orders = [self.fetch_order(i["symbol"], i["id"]) for i in orders]
        return await asyncio.gather(*orders)
    
    # 交易所接口中symbol是必须的
    @retry(stop_max_attempt_number=2, wait_fixed=100)
    async def _cancel_order_by_symbol(self, symbol, order_id=None):
        if order_id is None:
            return await self.api.cancelAllOrders(symbol)
        else:
            return await self.api.cancelOrder(order_id, symbol)
    
    # 取消不存在的订单会报错, 因此需要异常处理
    # TODO: order_id is None时的取消速度更快
    async def cancel_order_by_symbol(self, symbol, order_id=None):
        try:
            return await self._cancel_order_by_symbol(symbol, order_id)
        except Exception as e:
            self.logger.info(f"cancel {symbol} {order_id} failed {e}")
            return None
    
    async def cancel_orders_by_orders(self, orders):
        if not isinstance(orders, list):
            orders = [orders]
        cancel_orders = [self.cancel_order_by_symbol(i["symbol"], i["id"]) for i in orders]
        return await asyncio.gather(*cancel_orders)

    async def watch_tickers(self, symbols=None):
        try:
            if isinstance(symbols, str):
                return await self.api.watchTicker(symbols)
            elif symbols is None:
                return await self.api.watchTickers()
            else:
                return await self.api.watchTickers(symbols)
        except Exception as e:
            self.logger.info(f"watch tickers for {symbols} failed {e}")
            return None
        
    async def watch_balance(self, ):
        try:
            return await self.api.watchBalance()
        except Exception as e:
            self.logger.info(f"watch balance failed {e}")
            return None